﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Common.Utilities;
using Common.Utilities.DLR;

namespace Workflows.Components.ETL
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="webRequest"></param>
    public delegate void WebRequestIntercepted(WebRequest webRequest, WebResponse webResponse);
    /// <summary>
    /// 
    /// </summary>
    /// <param name="started"></param>
    public delegate void IntercepterStatusChanged(bool started);

    /// <summary>
    /// 
    /// </summary>
    public class WebRequestIntercepter
    {
        public event WebRequestIntercepted OnWebRequestIntercepted;
        public event IntercepterStatusChanged OnIntercepterStatusChanged;
        private Thread _SocketThread;
        private Socket _Socket;
        private bool _Started = false;

        /// <summary>
        /// 
        /// </summary>
        public bool Started
        {
            get
            {
                return this._Started;
            }
        }

        /// <summary>
        /// background listener and intercept recirected web requests
        /// </summary>
        public int PortNumber
        {
            get
            {
                int port = 8888;
                if(ConfigurationManager.AppSettings.Get("WebCrawlerIntercepterPortNumber")!=null)
                {
                    port = int.Parse(ConfigurationManager.AppSettings.Get("WebCrawlerIntercepterPortNumber"));
                }
                return port;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public string RedirectPlaceHolder
        {
            get
            {
                int port = this.PortNumber;
                string placeHolder = string.Format("@Redirect from: {0}:{1}", "localhost", port);
                return placeHolder;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="webRequest"></param>
        /// <returns></returns>
        public WebResponse GetWebResponse(WebRequest webRequest)
        {
            WebResponse webResponse =new WebResponse();
            webResponse.BaseUrl = webRequest.BaseUrl;
            webResponse.RelativePagePath = webRequest.RequestPagePath;
            webResponse.WebRequestID = webRequest.WebRequestID;
            webResponse.WebResponseID = Guid.NewGuid();
            webResponse.CachedContentFilePath = string.Empty;
            webResponse.Cookies=new List<Cookie>();

            string urlParameterString = string.Empty;
            if(webRequest.Parameters !=null && webRequest.Parameters.Count>0)
            {
                foreach(WebParameter parameter in webRequest.Parameters)
                {
                    if(parameter.ParemeterType==WebParameterType.Url)
                    {
                        if(urlParameterString.Length==0)
                        {
                            urlParameterString += "&";
                        }
                        urlParameterString += parameter.ParameterName + "=" + parameter.ParameterValue;
                    }
                }
            }
            string protocolString = webRequest.Protocol.ToString() + "://";
            HttpWebRequest req =
                (HttpWebRequest) HttpWebRequest.Create(protocolString + Uri.UnescapeDataString(webRequest.BaseUrl + webRequest.RequestPagePath +
                                                                              (string.IsNullOrEmpty(urlParameterString)
                                                                                   ? string.Empty
                                                                                   : "?" + urlParameterString)));
            req.Method = webRequest.Method.ToString();
            if(webRequest.Method==WebRequest.HttpMethod.POST)
            {
                string postedContent = webRequest.GetPostedContent();
                byte[] contents = Encoding.ASCII.GetBytes(postedContent);
                req.ContentLength = contents.Length;
                StreamWriter reqWriter = new StreamWriter(req.GetRequestStream());
                reqWriter.Write(postedContent);
                reqWriter.Flush();
                reqWriter.Close();
            }
            HttpWebResponse resp = (HttpWebResponse) req.GetResponse();
            StreamReader reader = new StreamReader(resp.GetResponseStream());
            string pageContent = reader.ReadToEnd();
            reader.Close();
            this.ReplaceRelativeUrls(webRequest.Protocol, webRequest.BaseUrl, webRequest.RequestPagePath, ref pageContent);
            if(this._Started)
            {
                this.AddRedirects(ref pageContent);
            }


            if(resp.Cookies !=null && resp.Cookies.Count>0)
            {
                List<Cookie> cookies=new List<Cookie>();
                foreach(Cookie cookie in resp.Cookies)
                {
                    cookies.Add(cookie);
                }
                webResponse.Cookies = cookies;
            }
            string tempFolderPath = Path.GetTempPath();
            string tempFilePath = Path.Combine(tempFolderPath, webResponse.WebResponseID.ToString() + ".html");
            if(File.Exists(tempFilePath))
            {
                File.Delete(tempFilePath);
            }
            StreamWriter writer=new StreamWriter(tempFilePath);
            writer.Write(pageContent);
            writer.Flush();
            writer.Close();
            webResponse.CachedContentFilePath = tempFilePath;

            ORMapping<WebRequest> orm1=new ORMapping<WebRequest>();
            orm1.Update(ConfigurationManager.ConnectionStrings["Workflow"].ConnectionString,ref webRequest);
            ORMapping<WebResponse> orm2=new ORMapping<WebResponse>();
            orm2.Update(ConfigurationManager.ConnectionStrings["Workflow"].ConnectionString, ref webResponse);
            return webResponse;
        }

        #region URL utilities
        /// <summary>
        /// 
        /// </summary>
        /// <param name="protocol"></param>
        /// <param name="baseUrl"></param>
        /// <param name="currentPageUrl"></param>
        /// <param name="pageContent"></param>
        public void ReplaceRelativeUrls(WebRequestProtocol protocol, string baseUrl, string currentPageUrl, ref string pageContent)
        {
            StringBuilder newPageContent = new StringBuilder();
            int pos = 0;

            // forms 
            Regex formRegex=new Regex("<form\\s+([^>]+)>",RegexOptions.IgnoreCase);
            Match formMatch = formRegex.Match(pageContent);
            while(formMatch.Success)
            {
                newPageContent.Append(pageContent.Substring(pos, formMatch.Index - pos));
                string formHeader = formMatch.Groups[1].Value;
                Regex actionRegex = new Regex("action\\s*=\\s*\'?\"?([^>\'\"]+)\'?\"?", RegexOptions.IgnoreCase);
                Match actionMatch = actionRegex.Match(formHeader);
                if(actionMatch.Success)
                {
                    string actionUrl = actionMatch.Groups[1].Value;
                    if (!this.IsAbsoluateUrl(actionUrl))
                    {
                        string absoluateActionUrl = this.GetAbsoluteUrl(protocol, baseUrl, currentPageUrl, actionUrl);
                        formHeader = formHeader.Replace(actionUrl, absoluateActionUrl);
                    }
                }

                newPageContent.Append("<form " + formHeader + ">");
                pos = formMatch.Index + formMatch.Value.Length;
                formMatch = formMatch.NextMatch();
            }
            if(pos<pageContent.Length)
            {
                newPageContent.Append(pageContent.Substring(pos));
            }
            pageContent = newPageContent.ToString();
            newPageContent=new StringBuilder();
            pos = 0;

            // hyperlinks
            Regex linkHeaderRegex=new Regex("<a\\s+([^>]+)>", RegexOptions.IgnoreCase);
            Match linkHeaderMatch = linkHeaderRegex.Match(pageContent);
            while(linkHeaderMatch.Success)
            {
                newPageContent.Append(pageContent.Substring(pos, linkHeaderMatch.Index-pos));

                string linkHeader = linkHeaderMatch.Groups[1].Value;
                Regex hrefRegex=new Regex("href\\s*=\\s*\'?\"?([^>\'\"]+)\'?\"?", RegexOptions.IgnoreCase);
                Match hrefMatch = hrefRegex.Match(linkHeader);
                if(hrefMatch.Success)
                {
                    string currentLinkUrl = hrefMatch.Groups[1].Value;
                    if(!this.IsAbsoluateUrl(currentLinkUrl))
                    {
                        string absoluateUrl = this.GetAbsoluteUrl(protocol, baseUrl, currentPageUrl, currentLinkUrl);
                        linkHeader = linkHeader.Replace(currentLinkUrl, absoluateUrl);
                    }
                }
                
                newPageContent.Append("<a " + linkHeader + ">");
                pos = linkHeaderMatch.Index + linkHeaderMatch.Value.Length;
                linkHeaderMatch = linkHeaderMatch.NextMatch();
            }
            if(pos<pageContent.Length)
            {
                newPageContent.Append(pageContent.Substring(pos));
            }
            pageContent = newPageContent.ToString();
            newPageContent=new StringBuilder();
            pos = 0;

            // images 
            Regex imgRegex=new Regex("<img\\s+([^>]+)>",RegexOptions.IgnoreCase);
            Match imgMatch = imgRegex.Match(pageContent);
            while(imgMatch.Success)
            {
                newPageContent.Append(pageContent.Substring(pos, imgMatch.Index-pos));

                string imgHeader = imgMatch.Groups[1].Value;
                Regex srcRegex = new Regex("src\\s*=\\s*\'?\"?([^\'\">]+)\'?\"?", RegexOptions.IgnoreCase);
                Match srcMatch = srcRegex.Match(imgHeader);
                if(srcMatch.Success)
                {
                    string imgUrl = srcMatch.Groups[1].Value;
                    if(!this.IsAbsoluateUrl(imgUrl))
                    {
                        string imageAbsoluteUrl = this.GetAbsoluteUrl(protocol, baseUrl,currentPageUrl, imgUrl);
                        imgHeader = imgHeader.Replace(imgUrl, imageAbsoluteUrl);
                    }
                }
                newPageContent.Append("<img " + imgHeader + ">");
                pos = imgMatch.Index + imgMatch.Value.Length;
                imgMatch = imgMatch.NextMatch();
            }
            if (pos < pageContent.Length)
            {
                newPageContent.Append(pageContent.Substring(pos));
            }
            pageContent = newPageContent.ToString();
            newPageContent = new StringBuilder();
            pos = 0;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        public bool IsAbsoluateUrl(string url)
        {
            if (url.ToLower().StartsWith("http://") || url.ToLower().StartsWith("https://"))
                return true;
            else if(url.StartsWith(".") || url.StartsWith("/"))
                return false;
            else
                return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="protocol"></param>
        /// <param name="baseUrl"></param>
        /// <param name="pageUrl"></param>
        /// <param name="linkUrl"></param>
        /// <returns></returns>
        public string GetAbsoluteUrl(WebRequestProtocol protocol, string baseUrl, string pageUrl, string linkUrl)
        {
            string[] folderNames = pageUrl.Split(new char[] {'/'});
            Stack<string> folderNameStack=new Stack<string>();
            foreach(string folderName in folderNames)
            {
                if(folderName.Trim().Length>0)
                {
                    folderNameStack.Push(folderName.Trim());
                }
            }

            string absoluateUrl = baseUrl;
            if (linkUrl.StartsWith("/"))
            {
                if (baseUrl.EndsWith("/"))
                {
                    absoluateUrl = baseUrl + linkUrl.Substring(1);
                }
                else
                {
                    absoluateUrl = baseUrl + linkUrl;
                }
            }
            else
            {
                while (linkUrl.StartsWith("../"))
                {
                    if (folderNameStack.Count > 0)
                    {
                        string parentFolderName = folderNameStack.Pop();
                        absoluateUrl += "/" + parentFolderName;
                        linkUrl = linkUrl.Substring("../".Length);
                    }
                }
                while (folderNameStack.Count > 0)
                {
                    string parentFolderName = folderNameStack.Pop();
                    absoluateUrl += "/" + parentFolderName;
                }
                if (absoluateUrl.EndsWith("/"))
                {
                    absoluateUrl += linkUrl;
                }
                else
                {
                    absoluateUrl += "/" + linkUrl;
                }
            }

            if(absoluateUrl.IndexOf("://")>0)
            {
                return absoluateUrl;
            }
            else
            {
                return protocol.ToString() + "://" + absoluateUrl;
            }
            
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="url"></param>
        /// <param name="baseUrl"></param>
        /// <param name="pageUrl"></param>
        /// <param name="parameters"></param>
        public void ParseUrl(string url, ref string baseUrl, ref string pageUrl, ref List<WebParameter> parameters)
        {
            bool hasDomainName = false;
            if(url.ToLower().StartsWith("http://"))
            {
                hasDomainName = true;
                url = url.Substring("http://".Length);
            }
            if(url.ToLower().StartsWith("https://"))
            {
                hasDomainName = true;
                url = url.Substring("https://".Length);
            }

            if(url.StartsWith("/")||url.StartsWith("."))
            {
                baseUrl = string.Empty;
                if(url.LastIndexOf("/")>0)
                {
                    pageUrl = url.Substring(0, url.LastIndexOf("/"));
                }
                else
                {
                    pageUrl = string.Empty;
                }
            }
            else if (url.IndexOf("/") > 0)
            {
                int domainEndPos = url.IndexOf("/");
                string domainName = url.Substring(0, domainEndPos);
                if (domainName.IndexOf(".") > 0)
                {
                    hasDomainName = true;
                }
                if (hasDomainName)
                {
                    baseUrl = domainName;
                    pageUrl = url.Substring(domainName.Length);
                }
                else
                {
                    pageUrl = url;
                }
            }
            else
            {
                baseUrl = url;
                pageUrl = string.Empty;
            }
            if (pageUrl.IndexOf("?") > 0)
            {
                pageUrl = pageUrl.Substring(0, pageUrl.IndexOf("?"));
            }
            // url parameters
            parameters = new List<WebParameter>();
            if(url.LastIndexOf("?")>0)
            {
                string paramString = url.Substring(url.LastIndexOf("?") + 1);
                string[] paramStrings = paramString.Split(new char[] {'&'});
                foreach(string paramStr in paramStrings)
                {
                    string[] nameValue = paramStr.Split(new char[] {'='});
                    if(nameValue.Length==2)
                    {
                        WebParameter webParameter=new WebParameter();
                        webParameter.ParameterName = nameValue[0];
                        webParameter.ParameterValue = nameValue[1];
                        webParameter.ParemeterType = WebParameterType.Url;
                        parameters.Add(webParameter);
                    }
                }
            }
        }
        #endregion

        #region add redirect to links/forms
        private void AddRedirects(ref string pageContent)
        {
            string redirect = string.Format("http://localhost:{0}/MyRedirect=", this.PortNumber);
            Regex protocolRegex = new Regex("http://([^\\s]+)|http://([^\\s]+)",RegexOptions.IgnoreCase);
            Match protocolMatch;
            StringBuilder newPageContent = new StringBuilder();
            int pos = 0;

            // forms 
            Regex formRegex = new Regex("<form\\s+([^>]+)>", RegexOptions.IgnoreCase);
            Match formMatch = formRegex.Match(pageContent);
            while (formMatch.Success)
            {
                newPageContent.Append(pageContent.Substring(pos, formMatch.Index - pos));
                string formHeader = formMatch.Groups[1].Value;
                Regex actionRegex = new Regex("action\\s*=\\s*\'?\"?([^>\'\"]+)\'?\"?", RegexOptions.IgnoreCase);
                Match actionMatch = actionRegex.Match(formHeader);
                if (actionMatch.Success)
                {
                    string actionUrl = actionMatch.Groups[1].Value;
                    protocolMatch = protocolRegex.Match(actionUrl);
                    if(protocolMatch.Success)
                    {
                        string replacement = redirect + protocolMatch.Value;
                        formHeader = formHeader.Replace(protocolMatch.Value, replacement);
                    }
                }
                newPageContent.Append("<form " + formHeader + ">");
                pos = formMatch.Index + formMatch.Value.Length;
                formMatch = formMatch.NextMatch();
            }
            if (pos < pageContent.Length)
            {
                newPageContent.Append(pageContent.Substring(pos));
            }
            pageContent = newPageContent.ToString();
            newPageContent = new StringBuilder();
            pos = 0;

            // hyperlinks
            Regex linkHeaderRegex = new Regex("<a\\s+([^>]+)>", RegexOptions.IgnoreCase);
            Match linkHeaderMatch = linkHeaderRegex.Match(pageContent);
            while (linkHeaderMatch.Success)
            {
                newPageContent.Append(pageContent.Substring(pos, linkHeaderMatch.Index - pos));

                string linkHeader = linkHeaderMatch.Groups[1].Value;
                Regex hrefRegex = new Regex("href\\s*=\\s*\'?\"?([^>\'\"]+)\'?\"?", RegexOptions.IgnoreCase);
                Match hrefMatch = hrefRegex.Match(linkHeader);
                if (hrefMatch.Success)
                {
                    string currentLinkUrl = hrefMatch.Groups[1].Value;
                    protocolMatch = protocolRegex.Match(currentLinkUrl);
                    if(protocolMatch.Success)
                    {
                        string replacement = redirect + protocolMatch.Value;
                        linkHeader = linkHeader.Replace(protocolMatch.Value, replacement);
                    }
                }
                newPageContent.Append("<a " + linkHeader + ">");
                pos = linkHeaderMatch.Index + linkHeaderMatch.Value.Length;
                linkHeaderMatch = linkHeaderMatch.NextMatch();
            }
            if (pos < pageContent.Length)
            {
                newPageContent.Append(pageContent.Substring(pos));
            }
            pageContent = newPageContent.ToString();
            newPageContent = new StringBuilder();
            pos = 0;

        }
        #endregion

        #region background socket listener
        /// <summary>
        /// start a http socket server listening intercepted message redirected
        /// </summary>
        public void Start()
        {
            this._SocketThread=new Thread(this.StartSocketListener);
            this._SocketThread.Start();
        }

        /// <summary>
        /// 
        /// </summary>
        public void Stop()
        {
            this.StopSocketListener();
        }

        private void StartSocketListener()
        {
            this._Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            try
            {
                IPEndPoint ipEndPoint = new IPEndPoint(Dns.GetHostAddresses("localhost")[0], this.PortNumber);
                this._Socket.Bind(ipEndPoint);
                this._Socket.Listen(1);
                Trace.WriteLine("Intercepter started listening at port {0}..." + this.PortNumber);
                this._Started = true;
                if (this.OnIntercepterStatusChanged != null)
                {
                    this.OnIntercepterStatusChanged(this._Started);
                }
                while (true)
                {
                    Trace.WriteLine("Intercepter is waiting for new connection...");
                    Socket handle = this._Socket.Accept();
                    StringBuilder buffer = new StringBuilder();
                    bool continueRead = true;
                    while (continueRead)
                    {
                        byte[] chunk = new byte[4096];
                        int bytesRead = handle.Receive(chunk,chunk.Length,SocketFlags.None);
                        if (bytesRead > 0)
                        {
                            buffer.Append(Encoding.ASCII.GetString(chunk, 0, bytesRead));

                            if(bytesRead<chunk.Length)
                            {
                                continueRead = false;
                            }
                        }
                        else
                        {
                            continueRead = false;
                        }
                    }
                    Trace.WriteLine("Intercepter received the following message:\n" + buffer.ToString());

                    try
                    {
                        string interceptedMessage = buffer.ToString();
                        if (interceptedMessage.IndexOf("/MyRedirect=") >= 0)
                        {
                            interceptedMessage = interceptedMessage.Replace("/MyRedirect=", "");
                        }
                        WebRequestParser parser = new WebRequestParser(interceptedMessage);
                        WebRequest webRequest = parser.WebReq;
                        WebResponse webResponse = this.GetWebResponse(webRequest);
                        if (this.OnWebRequestIntercepted != null)
                        {
                            this.OnWebRequestIntercepted(webRequest, webResponse);
                        }
                        StreamReader reader = new StreamReader(webResponse.CachedContentFilePath);
                        string responseContent = reader.ReadToEnd();
                        reader.Close();
                        byte[] bytesToSend = Encoding.ASCII.GetBytes(responseContent);
                        handle.Send(bytesToSend);
                    }
                    catch(Exception ex1)
                    {
                        ExceptionHandler.HandlesException(ex1);
                        Trace.WriteLine(ex1.ToString());
                    }
                    finally
                    {
                        handle.Shutdown(SocketShutdown.Both);
                        handle.Close();
                    }
                }
            }
            catch (Exception ex)
            {
                ExceptionHandler.HandlesException(ex);
                Trace.WriteLine(ex.ToString());
            }
        }

        private void StopSocketListener()
        {
            if(this._SocketThread !=null && this._SocketThread.IsAlive)
            {
                this._SocketThread.Abort();
            }
            if (this._Socket != null)
            {
                // this._Socket.Shutdown(SocketShutdown.Receive);
                this._Socket.Close();
            }

            this._Started = false;
            if (this.OnIntercepterStatusChanged != null)
            {
                this.OnIntercepterStatusChanged(this._Started);
            }
        }

        #endregion
    }
}
